ECSのまなび
概要
まなぶ。参考は凹みさんのやつ。
「Unity で Boids シミュレーションを作成して Entity Component System(ECS)を学んでみた」
http://tips.hecomi.com/entry/2018/12/23/200817
Boid、Simulation
他のすべてのBoidの位置を見たりして動くBoidと、
それらを生成するSimulationという組み合わせ。
ここから、ECSに向けて分解していく。
entityを導入
パッケージマネージャーから導入。
BoidのECS対応
Boid、中では
・加速度、位置などのパラメータがある
・関数でそれらの中に値をセットする
ということをやっていて、
これを、
パラメータ -> エンティティ化
関数 -> システム化
ということをやっていく。
動きのバリエーションとしては、
・壁に近づいたら云々
・向きとか速度を云々
・他の個体と近づく離れるを云々
する。
パラメータの分解粒度については、
・各種システムが必要な情報をグルーピングしてくるときに本当に必要な最小の粒度で分解した方が良い
ということなので、
まずは最小粒度でエンティティ化してみよう。
コンポーネントの作成
コンポーネント = ECSで扱う型情報の定義、みたいな印象。
IComponentDataを継承したstructを定義し、内部にUnity.Mathematics に定義されている型(float3とか)を保持する。
float3は、3つのfloatが入っている型。
起動クラスの作成
ECSの関連性を起動する()ような処理を書く。
・コンポーネントの定義
・デフォルトのWorldからEntityManagerを取得
・アーキタイプと呼ばれるものをmanagerにセット
・レンダラの生成
・エンティティの生成、コンポーネントの追加
コンポーネントの定義
public struct Velocity : IComponentData
{
public float3 Value;
}
public struct Acceleration : IComponentData
{
public float3 Value;
}
IComponentDataを継承した型を生成しておく。
この型をエンティティにセットすることで、エンティティに対して任意の値を持たせることができるようになる。
デフォルトのWorldからEntityManagerを取得
Worldは自分で生成することもできる。
var manager = World.Active.GetOrCreateManager<EntityManager>();
このmanagerに対して、レンダラやアーキタイプをセットしていく。
アーキタイプと呼ばれるものをmanagerにセット
var archetype = manager.CreateArchetype(
typeof(Position),
typeof(Rotation),
typeof(Scale),
typeof(Velocity),
typeof(Acceleration),
typeof(MeshInstanceRenderer));
managerに対して、用意したComponentをセットしてく。
Position、Rotation、Scale、MeshInstanceRendererはUnityが用意してくれているものを使用、
Velocity、Accelerationはさっき書いたものをセットする。
こうすることで、archetypeを生成できる。
archetypeはComponentの型を列挙したデータを含んでいるインスタンス。
レンダラの生成
var renderer = new MeshInstanceRenderer
{
castShadows = ShadowCastingMode.On,
receiveShadows = true,
mesh = mesh,
material = material
};
エンティティに対して描画時に使用する機構を生成しておく。
このrendererはあとでエンティティを生成する時にセットする。
エンティティの生成、コンポーネントの追加
var entity = manager.CreateEntity(archetype);
manager.SetComponentData(entity, new Position { Value = random.NextFloat3(1f) });
manager.SetComponentData(entity, new Rotation { Value = quaternion.identity });
manager.SetComponentData(entity, new Scale { Value = new float3(boidScale.x, boidScale.y, boidScale.z) });
manager.SetComponentData(entity, new Velocity { Value = random.NextFloat3Direction() * param.initSpeed });
manager.SetComponentData(entity, new Acceleration { Value = float3.zero });
manager.SetSharedComponentData(entity, renderer);
managerに対して、archetypeを渡すことでエンティティを生成する。
こうすることで、これらのコンポーネントを持ったEntityが生成される。
で、entityのそれぞれのコンポーネントに対してデータをセットすることができる。
最終的にentityに対してrendererをセットすると完了で、動き出す。
ここまでで、エンティティの生成は完了。
次に、これらのエンティティを動かす仕掛け(System)について考える。
エンティティを動かす
ComponentSystemを生成する。
・データ型をどこかに定義(外部でもいいはず)
・ComponentSystem型を継承した型を生成、データ型をクラス内のフィールドにインジェクト
・OnUpdate関数を書く
データ型をどこかに定義(外部でもいいはず)
こんな感じの型を作る。
struct Data
{
public readonly int Length;
public ComponentDataArray<Position> positions;
[WriteOnly] public ComponentDataArray<Rotation> rotations;
public ComponentDataArray<Velocity> velocities;
public ComponentDataArray<Acceleration> accelerations;
}
Lengthには、自動的に「何個のEntityがあるか」が入る。つまりこの数分だけエンティティがある、という情報を取得できる。
positionとかがarrayになっていて、「すべてのエンティティのpositon」が入っているアレイになっている。すごい。
ComponentSystem型を継承した型を生成、データ型をクラス内のフィールドにインジェクト
public class WallSystem : ComponentSystem
{
struct Data
{
public readonly int Length;
[ReadOnly] public ComponentDataArray<Position> positions;
public ComponentDataArray<Acceleration> accelerations;
}
[Inject] Data data;
protected override void OnUpdate()
{
.....
for (int i = 0; i < data.Length; ++i)
{
data.accelerations[i] = new Acceleration { Value = accel };
}
}
}
OnUpdateが毎フレーム実行される。
Data型をInjectアトリビュートをつけて定義しているが、このインスタンスに対して自動的にエンティティが入る。
で、MoveとWallをセットすることで、移動と壁接近の処理を実現できる。
これらのComponentSystemを継承した型は、自動的にデフォルトのWorldに組み込まれる。
ここまでで壁接近と移動に関してのシステムができた。
で、その実行順の制御をいまのところはMoveSystemにattributeで
[UpdateAfter(typeof(WallSystem))]
とかやって行う。
ここからは、一つのエンティティから別のエンティティを参照して、群(Boid)っぽい挙動をさせる。
他の個体と近づく離れるを云々 の部分。
この場合、一つのエンティティから、ほかの(近所の)エンティティを、配列として扱うことになる。
配列をエンティティ内で扱う
データ型を定義する。
[InternalBufferCapacity(8)]
public struct NeighborsEntityBuffer : IBufferElementData
{
public Entity Value;
}
IBufferElementDataを継承したコンポーネントを用意する。これで配列が扱える。
今まではIComponentData。
Entityを値として持つ。(配列として扱われる。)
アトリビュート InternalBufferCapacity はIBufferElementDataに対してセットする。
この数値を超えた場合はデータがC#のヒープにいってしまって遅くなるっぽい。
アーキタイプにNeighborsEntityBuffer型を足す。
var archetype = manager.CreateArchetype(
typeof(Position),
typeof(Rotation),
typeof(Scale),
typeof(Velocity),
typeof(Acceleration),
typeof(MeshInstanceRenderer),
typeof(NeighborsEntityBuffer)
);
NeighborsEntityBuffer 型に関しては、CreateEntity時にデータをセットするような要素がないのでこのまま。
IBufferElementDataを継承した型の中身は、自動的に配列として扱われる。
この場合は、Entity型のアレイになる。
Bufferからデータを取り出す場合、Reinterpret<バッファに含まれている型> で、型のアレイが取り出せる。
/*
neighborsから一つを取り出す。ここでNeighborsEntityBufferの中身はEntityだけなので、
Reinterpret<型>で指定するとEntityのアレイが手に入る。
*/
var neighbors = data.neighbors[i].Reinterpret<Entity>();
これで、neighborsは DynamicBuffer<Entity> 型、Entityのアレイになる。
で、エンティティからPositionなどの一パラメータを取得するには、 EntityManager.GetComponentData<Position>(対象のエンティティ) とかやる。
このほかにAlignment(整列)とCohesion(結束)のSystemを作る。
順番付け
この時点で複数のシステムがあって、順番問題が出てくる。
別に複数のシステムに分けなくてもいいはずなんで、まとめてもいいんだけど、ここでは
・特定のまとまり
・その前
・そのあと
というような区分けで順番付けを行うことができる。
[UpdateInGroup(typeof(クラス))]
とかで、特定のクラス定義を基にしたグループを生成できる。
このグループ内だとなんかどういう順番での実行になるかわからないが、
[UpdateAfter(typeof(クラス))]
とかやると、グループの実行後に実行、
[UpdateBefore(typeof(クラス))]
でグループの実行前に実行、とかができる。
グループが生成できるのがいいところ。
ECSといってもメインスレッドで動くのがデフォルトなんだな~
というわけで、ここからは別スレッドでの実行にしていく。
WallSystemのベースクラスを ComponentSystem から JobComponentSystem に載せ替えて、OnUpdateがJobHandleを返すようにセットする。
public struct Job : IJobParallelFor
{
// OnUpdate() から渡してもらう
[ReadOnly] public ComponentDataArray<Position> positions;
[ReadOnly] public float scale;
[ReadOnly] public float thresh;
[ReadOnly] public float weight;
public ComponentDataArray<Acceleration> accelerations;
// [ReadOnly] public Param param;
// OnUpdate() の処理を移植
public void Execute(int index)
{
float3 pos = positions[index].Value;
float3 accel = accelerations[index].Value;
accel +=
GetAccelAgainstWall(-scale - pos.x, new float3(+1, 0, 0), thresh, weight) +
GetAccelAgainstWall(-scale - pos.y, new float3(0, +1, 0), thresh, weight) +
GetAccelAgainstWall(-scale - pos.z, new float3(0, 0, +1), thresh, weight) +
GetAccelAgainstWall(+scale - pos.x, new float3(-1, 0, 0), thresh, weight) +
GetAccelAgainstWall(+scale - pos.y, new float3(0, -1, 0), thresh, weight) +
GetAccelAgainstWall(+scale - pos.z, new float3(0, 0, -1), thresh, weight);
accelerations[index] = new Acceleration { Value = accel };
}
float3 GetAccelAgainstWall(float dist, float3 dir, float thresh, float weight)
{
if (dist < thresh)
{
return dir * (weight / math.abs(dist / thresh));
}
return float3.zero;
}
}
JobSystemを使って並列化(メインスレッド以外での実行)するために、IJobParallelFor型を継承したstructを用意する。
Execute関数の中で、Updateで実行していた要素を実行する。
Execute関数はエンティティ分だけ実行される。
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new Job
{
positions = data.positions,
accelerations = data.accelerations,
// IJobParallelForを継承したstructは参照型の参照を持てないので、ここで値を渡す。
scale = Bootstrap.Param.wallScale * 0.5f,
thresh = Bootstrap.Param.wallDistance,
weight = Bootstrap.Param.wallWeight
};
return job.Schedule(data.Length, 32, inputDeps);
}
これで、WallSystemでのOnUpdate -> Job、という形でデータが別スレッドで処理されるようになる。
素敵。
もっとECSに向いたJob関連の仕組みがあるので、そちらを使って全体を移行する。
IJobProcessComponentData型を使ってジョブ化を行う
IJobProcessComponentData
型を使ってジョブの定義を行うと、引数を持たせた上でExecuteの定義ができる。
次のように型引数をセットして定義、
private struct Job : IJobProcessComponentData<Position, Rotation, Velocity, Acceleration>
{
[ReadOnly] public float dt;
[ReadOnly] public float minSpeed;
[ReadOnly] public float maxSpeed;
public void Execute(ref Position pos, [WriteOnly] ref Rotation rot, ref Velocity vel, ref Acceleration accel)
{
var v = vel.Value;
v += accel.Value * dt;
var dir = math.normalize(v);
var speed = math.length(v);
v = math.clamp(speed, minSpeed, maxSpeed) * dir;
pos = new Position { Value = pos.Value + v * dt };
rot = new Rotation { Value = quaternion.LookRotationSafe(dir, new float3(0, 1, 0)) };
vel = new Velocity { Value = v };
accel = new Acceleration { Value = float3.zero };
}
}
Executeはやはりエンティティ数分だけ実行される。
jobを返すのを要求している JobComponentSystem のOnUpdateで、パラメータを含んだjobを生成し、Scheduleを返す。
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new Job
{
dt = Time.deltaTime,
minSpeed = Bootstrap.Param.minSpeed,
maxSpeed = Bootstrap.Param.maxSpeed,
};
return job.Schedule(this, inputDeps);
}
Execute関数にはPositionやら何やらが型定義時点の要素で入ってくるので、パラメータの特性をAttributeでセットする。
この場合はRotationがWriteしかしないので、WriteOnlyにする。
このExecute関数で渡ってくるrefは、Injectしていた時の要素と同じような感じ。
OnUpdate関数がメインスレッドで、そこからJobSystemへとジョブを送り出す。
JobSystem側では別スレッド(WorkerThread)経由でExecuteが呼ばれ、そこで処理を行う。
配列を扱う(特にEntityの配列)
SeparationSysytemをJob化する。
neighborsがEntityのアレイなので、その辺を扱う必要がある。
IJobProcessComponentDataWithEntity
型でjob用のデータを定義すると、Executeの第一引数にEntity型、第二引数にint indexがつく。
それ以降の引数は定義型と一致する。
public void Execute(Entity entity, int index, [ReadOnly] ref Position pos, ref Acceleration accel){
こんな感じ。
OnUpdateではjobを生成してスケジュールを返すんだが、
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
// パラメータをつけてJobSystemに放り込む
var job = new Job
{
separationWeight = Bootstrap.Param.separationWeight,
/*
JobComponentSystem が GetBufferFromEntity メソッドを持っているので、
NeighborsEntityBufferの型指定でエンティティからバッファを取り出す。
エンティティをキーにしたNeighborsの一覧という形で、ジョブの実行先で取り出すのを予定する。
*/
neighborsFromEntity = GetBufferFromEntity<NeighborsEntityBuffer>(true),
/*
Position型指定でエンティティからコンポーネントを取り出す。
エンティティをキーにしたポジションの一覧という形で、ジョブの実行先で取り出すのを予定する。
*/
positionFromEntity = GetComponentDataFromEntity<Position>(true),
};
return job.Schedule(this, inputDeps);
}
IJobProcessComponentDataWithEntity を継承したstruct側では、そのExecuteでentityを含んだ実行ブロックになる。
public struct Job : IJobProcessComponentDataWithEntity<Position, Acceleration>
{
[ReadOnly] public float separationWeight;
[ReadOnly] public BufferFromEntity<NeighborsEntityBuffer> neighborsFromEntity;
[ReadOnly] public ComponentDataFromEntity<Position> positionFromEntity;
public void Execute(Entity entity, int index, [ReadOnly] ref Position pos, ref Acceleration accel)
{
var neighbors = neighborsFromEntity[entity].Reinterpret<Entity>();
if (neighbors.Length == 0)
{
return;
}
var pos0 = pos.Value;
var force = float3.zero;
for (int i = 0; i < neighbors.Length; ++i)
{
var pos1 = positionFromEntity[neighbors[i]].Value;
force += math.normalize(pos0 - pos1);
}
force /= neighbors.Length;
var dAccel = force * separationWeight;
accel = new Acceleration { Value = accel.Value + dAccel };
}
}
で、neighborsFromEntity は辞書みたいな形になっていて、
エンティティ分だけ実行されるExecuteに対して、entityをキーとしてneighborsのバッファを取得することができる。
同じくpositionもエンティティをキーとした辞書にしてあって、取得できる。
最終的に、accelの値を更新して終了。
最後に、NeighborDetectionSystemをJob化する。
NeighborDetectionSystemをJob化
IJobProcessComponentDataWithEntity を使ってJobの定義を行う。
で、NeighborDetectionSystemについては、
・エンティティの近所にあるエンティティを集める
という必要性があるため、収集のための機構を作る必要がある。
ComponentGroup group;
protected override void OnCreateManager()
{
/*
ComponentSystemBaseに生えてるメソッド GetComponentGroup で、特定のエンティティを集める。
*/
group = GetComponentGroup(
typeof(Position),
typeof(Velocity),
typeof(NeighborsEntityBuffer));
}
ComponentSystemBase にあるGetComponentGroupメソッドで、
グループという単位を作り Position、Velocity、NeighborsEntitiyBuffer コンポーネントをくくり出せるようにしている。
group.GetEntityArray()
で、Position、Velocity、NeighborsEntitiyBuffer コンポーネントを持ったエンティティを収集できる。
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new Job
{
prodThresh = math.cos(math.radians(Bootstrap.Param.neighborFov)),
distThresh = Bootstrap.Param.neighborDistance,
neighborsFromEntity = GetBufferFromEntity<NeighborsEntityBuffer>(false),
positionFromEntity = GetComponentDataFromEntity<Position>(true),
entities = group.GetEntityArray(),
};
return job.Schedule(this, inputDeps);
}
jobを生成。entitiesパラメータに取得したエンティティを入れる。
jobの定義は次のような感じで、EntityArray entitiesにgroupから取得したエンティティが入る。
public struct Job : IJobProcessComponentDataWithEntity<Position, Velocity>
{
[ReadOnly] public float prodThresh;
[ReadOnly] public float distThresh;
[ReadOnly] public ComponentDataFromEntity<Position> positionFromEntity;
[ReadOnly] public BufferFromEntity<NeighborsEntityBuffer> neighborsFromEntity;
[ReadOnly] public EntityArray entities;
public void Execute(
Entity entity,
int index,
[ReadOnly] ref Position pos,
[ReadOnly] ref Velocity velocity)
{
neighborsFromEntity[entity].Clear();
float3 pos0 = pos.Value;
float3 fwd0 = math.normalize(velocity.Value);
for (int i = 0; i < entities.Length; ++i)
{
var neighbor = entities[i];
if (neighbor == entity) continue;
float3 pos1 = positionFromEntity[neighbor].Value;
var to = pos1 - pos0;
var dist = math.length(to);
if (dist < distThresh)
{
var dir = math.normalize(to);
var prod = Vector3.Dot(dir, fwd0);
if (prod > prodThresh)
{
neighborsFromEntity[entity].Add(new NeighborsEntityBuffer { Value = neighbor });
}
}
}
}
}
Executeごとにentitiesの中身を捜査して近いエンティティを見つけ、
自身のneighborsに入れる。
という感じで、収集ができる。
ここまでのまとめ
コンポーネント(ECSで扱うデータ型)の定義
IComponentData型を継承して、型を定義する。(Unityが定義済みの型があり、それ以外にも定義できる)
IBufferElementData型を継承して、扱う配列を定義する。
InternalBufferCapacityアトリビュートでをセットする。
エンティティの生成
ワールド、エンティティマネージャーを生成
アーキタイプ(エンティティに対してくっつけるコンポーネントの集合)を定義
マネージャ経由でアーキタイプからエンティティ生成
ECS2パターン
パターン1、ComponentSystem型を継承したシステムを定義、Injectしたデータを void OnUpdate で変形したりする(メインスレッド、ちょっとは高速)
この場合、定義したシステムに対してコンポーネントの定義を別途Injectすると、そのフィールドに対してOnUpdateのタイミングでエンティティが入り、変更を加えることができるようになる。
変更すると、エンティティにも値が反映される。
パターン2、JobComponentSystem型を継承したシステムを定義、次のどれかの型の継承でJobを定義、JobHandle OnUpdate(JobHandle handle) でJob作成
-> 投入、Job側のExecuteで(workerスレッド、高速)
IJobParallelFor Execute(int index)
IJobProcessComponentData<T(, ~ T3) Execute(ref T t)
IJobProcessComponentDataWithEntity<T(, ~ T3)> Execute(Entity entity, int index, ref T t)
T1~3にはReadOnlyなどのアトリビュートをつけることで、アクセスの最適化を行う。
JobはBurstCompiler対応でめっちゃ高速化される。
値に対して、Job内でパラメータを編集するという形で値の反映を行う。
双方とも、OnUpdateメソッドはメインスレッドで1フレームに一回実行される。
Jobがメインスレッドからしか放り込めないのに起因してる。
JobのExecuteはヒットしたエンティティの数ぶん実行される。
IJobProcessComponentData<T>などのTに該当するエンティティ全てが対象になる。
Tは扱いたいデータ自体の選出のほか、フィルタリングにも使っている。
特殊な型ペイロード
ComponentDataFromEntity<T>
entityをキーに、値として T を取り出すことができる辞書。
ComponentSysytem型が保持しているGetComponentDataFromEntity<T>(bool isReadOnly) メソッドで取得できる。
ここで取得できるのは、その時のWorldに存在するT型のエンティティを収集した辞書。
BufferFromEntity<T>
entityをキーに、値として DynamicBuffer<T> を取り出すことができる辞書。
JobComponentSystem型が保持しているGetBufferFromEntity<T>(bool isReadOnly) メソッドで取得できる。
ここで取得できるのは、その時のWorldに存在するT型のエンティティを収集した辞書。
DynamicBuffer<T>
BufferFromEntity<T>型から、Reinterpret<S> (SはTに含まれている型)とかで、DynamicBuffer<S> を取り出せる。
Jobの制約
Jobの内部では、エンティティ収集などができない。
収集処理とかはメインスレッド = ComponentSystemのOnUpdateとか で行う必要がある。
ComponentSystem/OnUpdateは、参照とかをJobに放り込む地点になる。
ざっくり書くとどういうことをやっているか
・コンポーネント型を定義(バッファとかデータとか)
・コンポーネントを保持したエンティティ生成
・システムを定義して、特定のコンポーネントを持つエンティティを収集、OnUpdateでデータをいじったり、データをいじるJobを作り、JobSystemに投入、実行
こんだけ。
コンポーネントの型をキーにWorldに散らばっているエンティティを収集して、JobSystem上で値を変えることで描画に影響を与える。
別のSystemにわけて書いてあるものをまとめる
複数のJobをそれぞれ1つのSystemに別れて定義しているが、特にわかりやすい以外の利点はない。
で、これらをまとめて一つのSystemに定義することができる。
そうすることで、システム間の待ちを根本的に減らせる。
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
/*
近いエンティティの収集
*/
var neighbors = new NeighborsDetectionJob
{
prodThresh = math.cos(math.radians(Bootstrap.Param.neighborFov)),
distThresh = Bootstrap.Param.neighborDistance,
neighborsFromEntity = GetBufferFromEntity<NeighborsEntityBuffer>(false),
positionFromEntity = GetComponentDataFromEntity<Position>(true),
entities = group.GetEntityArray(),
};
/*
壁処理
*/
var wall = new WallJob
{
scale = Bootstrap.Param.wallScale * 0.5f,
thresh = Bootstrap.Param.wallDistance,
weight = Bootstrap.Param.wallWeight,
};
/*
近所のエンティティから離れる
*/
var separation = new SeparationJob
{
separationWeight = Bootstrap.Param.separationWeight,
/*
JobComponentSystem が GetBufferFromEntity メソッドを持っているので、
NeighborsEntityBufferの型指定でエンティティからバッファを取り出す。
エンティティをキーにしたNeighborsの一覧という形で、ジョブの実行先で取り出すのを予定する。
*/
neighborsFromEntity = GetBufferFromEntity<NeighborsEntityBuffer>(true),
/*
Position型指定でエンティティからコンポーネントを取り出す。
エンティティをキーにしたポジションの一覧という形で、ジョブの実行先で取り出すのを予定する。
*/
positionFromEntity = GetComponentDataFromEntity<Position>(true),
};
/*
均整化
*/
var alignment = new AlignmentJob
{
alignmentWeight = Bootstrap.Param.alignmentWeight,
neighborsFromEntity = GetBufferFromEntity<NeighborsEntityBuffer>(true),
velocityFromEntity = GetComponentDataFromEntity<Velocity>(true),
};
/*
距離調整
*/
var cohesion = new CohesionJob
{
cohesionWeight = Bootstrap.Param.cohesionWeight,
neighborsFromEntity = GetBufferFromEntity<NeighborsEntityBuffer>(true),
positionFromEntity = GetComponentDataFromEntity<Position>(true),
};
/*
移動
*/
var move = new MoveJob
{
dt = Time.deltaTime,
minSpeed = Bootstrap.Param.minSpeed,
maxSpeed = Bootstrap.Param.maxSpeed,
};
// 順番に動作を行うように、ジョブを連結してスケジューリングする。
inputDeps = neighbors.Schedule(this, inputDeps);
inputDeps = wall.Schedule(this, inputDeps);
inputDeps = separation.Schedule(this, inputDeps);
inputDeps = alignment.Schedule(this, inputDeps);
inputDeps = cohesion.Schedule(this, inputDeps);
inputDeps = move.Schedule(this, inputDeps);
// 最終的なJobHandleを返す。
return inputDeps;
}
JobComponentSystemのOnUpdate関数でそれぞれのJobの初期化をし、Scheduleメソッドでチェーンしまくる。
連結してSchedulingすることができる。
JobをBurstCompileで高速化
BurstCompileアトリビュートをJob定義につける。そんだけで高速化。